#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <assert.h>

#include "proxy.h"

#define MAX_CONFIG_LINES 1024


/* Tokenize line into many small pieces separated by any of sep_chars. On
   occurrence of quit_char stop tokenizing. */
int split_line(char *argv[], int argv_sz, char *line, char *sep_chars, char *quit_chars) {
	assert(argv_sz > 0);

	int argc = 0;
	char *line_end = line + strlen(line);
	char sep[256] = {0};
	while(*sep_chars) {
		int p = *sep_chars++;
		sep[p] = 1;
	}
	while(*quit_chars) {
		int p = *quit_chars++;
		sep[p] = 2;
	}
	
	char prev = 0;
	for(; line < line_end; line++) {
		switch(sep[(int)*line]) {
		case 1:
			*line = '\0'; // inplace
			break;
		case 2:
			*line = '\0';
			goto done;
		}
		if(!prev && *line) { // new block
			argv[argc++] = line;
			if(argc == argv_sz)
				goto done;
		}
		prev = *line;
	}
done:
	return(argc);
}

int get_host_port(char **host_ptr, int *port_ptr, char *line) {
	char *argv[2];
	int argc = split_line(argv, NELEM(argv), line, ":", "");
	if(argc != 2)
		return(-1);
	*host_ptr = argv[0];
	*port_ptr = atoi(argv[1]);
	if(*port_ptr < 1 || *port_ptr > 65535)
		return(-2);
	return(0);
}


char *load_config_from_string(struct config *config, char *buf, int testing) {
	char *err = "unknown error";
	char *host, *key;
	int port, key_sz;
	int proxies = 0, servers = 0;
	
	char **lines = (char**)malloc(sizeof(char*) * MAX_CONFIG_LINES);
	int lines_no = split_line(lines, MAX_CONFIG_LINES, buf, "\n\r", "");
	
	int line_no = 0;
	for(line_no = 0; line_no < lines_no; line_no++) {
		char *line = lines[line_no];
		char *argv[32];
		int argc = split_line(argv, NELEM(argv), line, " ,\t", "#");
		if(argc == 0)
			continue;
		if(0 == strcmp(argv[0], "proxy")) {
			if(argc != 1+1) {
				err = "Expected exactly one argument";
				goto error;
			}
			if(0 != get_host_port(&host, &port, argv[1])) {
				err = "Bad host description or port number";
				goto error;
			}
			if(!testing && -1 == new_st_proxy(config, host, port)){
				err = "Duplicate proxy";
				goto error;
			}
			proxies++;
		}else
		if(0 == strcmp(argv[0], "server")) {
			if(argc != 3+1 && argc != 2+1) {
				err = "Expected two or three arguments";
				goto error;
			}
			if(0 != get_host_port(&host, &port, argv[1])) {
				err = "Bad host description or port number";
				goto error;
			}
			int weight = atoi(argv[2]);
			if(weight < 1 || weight > 999999) {
				err = "Broken weight value";
				goto error;
			}
			if(argc == 3+1) {
				if(0 != decode_hex(&key, &key_sz, argv[3])) {
					err = "Broken key value";
					goto error;
				}
			} else {
				key = "";
				key_sz = 0;
			}
			if(!testing && -1 == new_st_server(config, host, port, weight, key, key_sz)) {
				err = "Duplicate server";
				goto error;
			}
			servers++;
		} else {
			err = argv[0];//"Unrecognized keyword";
			goto error;
		}
	}
	if(!proxies) {
		err = "No proxies defined in config, not even myself!\n"
			"Are you sure it's a correct autogenerated file?";
		goto error;
	}
	if(!servers) {
		err = "No servers defined in config. It's broken!";
		goto error;
	}
	free(lines);
	return(NULL);
error:;
	free(lines);
	static char line[256];
	snprintf(line, sizeof(line), "%s on line %i", err, line_no+1);
	return(line);
}


void load_config(struct config *config) {
	char *data = read_full_file(config->config_path);
	if(NULL == data) {
		fatal("Can't read config file \"%s\".", config->config_path);
	}
	
	char *err = load_config_from_string(config, data, 0);
	if(err) {
		log_error("%s", err);
		fatal("Broken config file \"%s\".", config->config_path);
	}
	
	free(data);
}

void unload_config(struct config *config) {
	free_all_servers(config);
	free_all_proxies(config);
}

char *strip(char *s) {
	char replace_chars[256] = {
		['\n']	1,
		['\r']	1,
		['\t']	1,
		[' ']	1
	};
		
	while(replace_chars[(int)*s])
		s++;
	int len = strlen(s);
	while(len > 0 && replace_chars[(int)*(s+len-1)]) {
		s[len-1] = '\0';
		len--;
	}
	return(s);
}

int write_line(char **buf_ptr, int *buf_sz_ptr, const char *fmt, ...) __attribute__ ((format (printf, 3, 4)));
int write_line(char **buf_ptr, int *buf_sz_ptr, const char *fmt, ...) {
	va_list ap;
	va_start(ap, fmt);
	int r = vsnprintf(*buf_ptr, *buf_sz_ptr, fmt, ap);
	va_end(ap);

	if(r > *buf_sz_ptr)
		return(-1);
	*buf_sz_ptr -= r;
	*buf_ptr += r;
	return(r);
}

int config_to_string(struct config *config, char *org_buf, int org_buf_sz) {
	int r = 0;
	char *buf = org_buf;
	int buf_sz = org_buf_sz;
	r = write_line(&buf, &buf_sz, "# This file is automatically generated. Do not edit.\n");
	if(r < 0) goto error;
	char hostname[256];
	gethostname(hostname, sizeof(hostname));
	time_t now = time(NULL);
	r = write_line(&buf, &buf_sz, "#Created on %s, %s\n\n", strip(ctime(&now)), hostname);
	if(r < 0) goto error;
	
	struct rb_node *node = rb_first(&config->proxies);
	while(node) {
		struct st_proxy *proxy = container_of(node, struct st_proxy, node);
		node = rb_next(node);
		r = write_line(&buf, &buf_sz, "proxy\t%s:%i\n", proxy->host, proxy->port);
		if(r < 0) goto error;
	}
	
	r = write_line(&buf, &buf_sz, "\n");
	if(r < 0) goto error;
	node = rb_first(&config->servers);
	while(node) {
		struct st_server *server = container_of(node, struct st_server, node);
		node = rb_next(node);
		r = write_line(&buf, &buf_sz, "server\t%s:%i, %i, %s\n",
					server->host, server->port,
					server->weight,
					encode_hex(server->key, server->key_sz));
		if(r < 0) goto error;
	}
	return(org_buf_sz - buf_sz);
error:
	log_error("buffer too small");
	return(-1);
}

#define MAX_CONFIG_SIZE (MAX_CONFIG_LINES*1024)

int save_config(struct config *config) {
	FILE *fd;
	fd = fopen(config->config_path_new, "w");
	if(NULL == fd) {
		log_error("Can't write to config file \"%s\".", config->config_path_new);
		return(-1);
	}
	char *buf = (char*)malloc(MAX_CONFIG_SIZE);
	int r = config_to_string(config, buf, MAX_CONFIG_SIZE);
	if(r < 0)
		goto error;
	buf[MAX_CONFIG_SIZE-1] = '\0';
	fputs(buf, fd);
	
	free(buf);
	fclose(fd);
	if(0 != rename(config->config_path_new, config->config_path)) {
		log_perror("rename(%s, %s)", config->config_path_new, config->config_path);
	}
	return(0);
	
error:;
	free(buf);
	fclose(fd);
	log_error("Can't write to config file \"%s\".", config->config_path_new);
	return(-1);
}


void flush_config(struct config *config) {
	free_all_servers(config);
	free_all_proxies(config);
}